import BpmnModeler from 'bpmn-js/lib/Modeler'
import minimapModule from 'diagram-js-minimap'
// import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
// import { is } from 'bpmn-js/lib/util/ModelUtil'

import customTranslateModule from '../translations'

import { debounce } from '../../../utils/throttle-debounce'

// #endregion

export class UBpmnModeler extends BpmnModeler {
  /** @type {import('../types/draw').Canvas} */
  canvas
  /**
   * 建模器（绘图用）
   * @type {import('../types').Modeling}
   */
  modeling
  /**
   * 用于生成、管理bpmn专用xml文件，如过需要把属性放到xml中，则需要用到moddle
   * @type {import('../types').BpmnModdle}
   */
  moddle
  /**
   * 元素注册表
   * @type {import('../types').ElementRegistry}
   */
  elementRegistry
  /**
   * 事件总线
   * @type {import('../types').EventBus}
   */
  eventBus
  /**
   * @type {import('../types').CommandStack}
   */
  commandStack
  /**
   * @type {Element}
   */
  get container() {
    return this._container
  }
  get zoomLevel() {
    return this.canvas.viewbox().scale
  }
  /**
   * @param {Object} [options]
   * @param {HTMLElement|string} [options.container] 容器，默认为body
   * @param {string|number} [options.width] 视图宽度
   * @param {string|number} [options.height] 视图高度
   * @param {string} [options.xml] bpmn的xml模型，为空则创建一个新的流程图
   * @param {string} [options.lang] 语言类型
   * @param {boolean} [options.customPropPanel] 使用自定义属性面板，否则使用bpmn-js-properties-panel（请使用npm安装）
   * @param {Object} [options.moddleExtensions] 扩展包
   * @param {Array<didi.Module>} [options.modules] 覆盖默认模块的模块列表
   * @param {Array<didi.Module>} [options.additionalModules] 与默认模块一起使用的模块列表
   * @param {(template:string, replacements:string)=>string} [options.customTranslate] 自定义翻译
   */
  constructor(options) {
    options.additionalModules = options.additionalModules ?? []
    options.moddleExtensions = options.moddleExtensions ?? {}

    options.container =
      options.container instanceof HTMLElement
        ? options.container
        : typeof options.container === 'string'
        ? document.querySelector(options.container) || document
        : document

    if (!options.customPropPanel) {
      // 如何添加属性面板: https://github.com/bpmn-io/bpmn-js-properties-panel
      try {
        options.additionalModules.push(require('bpmn-js-properties-panel'))
        // bpmn
        // options.additionalModules.push(require('bpmn-js-properties-panel/lib/provider/bpmn'))
        // camunda
        options.additionalModules.push(require('bpmn-js-properties-panel/lib/provider/camunda'))
        import('bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css')
      } catch (error) {
        console.error('未安装bpmn-js-properties-panel')
      }
      try {
        options.moddleExtensions.camunda = require('camunda-bpmn-moddle/resources/camunda')
      } catch (error) {
        console.error('未安装camunda-bpmn-moddle')
      }
    }

    if (options.customTranslate) {
      options.additionalModules.push({ translate: ['value', options.customTranslate] })
      delete options.customTranslate
    } else {
      options.additionalModules.push(customTranslateModule(options.lang))
    }

    options.additionalModules.push(minimapModule)
    /**
     * 使用bpmnjs的时候发现，按下键盘无论如何都没法触发内部的快捷键(例如:Ctrl+Z、Ctrl+Y、Delete)，
     * 在容器的div上加tabindex也没用，去看官方的例子，也没找到相关内容
     * 最后让我扒官方demo的源码扒到了，我真的服气，就完全没介绍这个参数！
     * keyboard需要传入bindTo参数，这是事件源
     * （这个参数在在diagram-js源码的lib\features\keyboard\Keyboard.js 69行处被使用）
     */
    options.keyboard = {
      bindTo: options.container
    }
    super(options)

    this.options = options
    this.container.setAttribute('tabindex', '0')

    this.on('import.done', () => {
      // 调整画布位置，适应当前可视区域
      this.canvas.zoom('fit-viewport')
    })

    this.init()
    this.removePowerBy()

    return this
  }
  init() {
    try {
      this.canvas = this.get('canvas')
      this.eventBus = this.get('eventBus')
      this.elementRegistry = this.get('elementRegistry')
      this.modeling = this.get('modeling')
      this.moddle = this.get('moddle')
      this.commandStack = this.get('commandStack')

      // #region 触发导入完成事件
      let events = ['shape.added', 'element.changed']
      // 解释下为什么这么做：
      // 在导入图形import.done事件之后，bpmn-js依然会触发shape.added事件
      // 通过消抖来处理连续shape.added事件
      // 在最后一个shape.added事件触发60ms后触发import.finshed
      let imporFinshedDeb = debounce(() => {
        console.log('import.finshed 消抖触发')

        this.off(events, imporFinshedDeb)
        this.fire('import.finshed')
      }, 60)

      this.on('import.done', () => {
        console.log('bpmn -> import.done')

        // 先调第一次，防止创建图形时不会触发element.changed
        imporFinshedDeb()

        this.on(events, imporFinshedDeb)
      })
      // #endregion
    } catch (error) {
      console.error('bpmn初始化失败：', error)
    }
  }
  changeShortcuts() {
    // 见diagram-js源码：lib\features\keyboard\KeyboardBindings.js 72）
    // 目前还没找到修改方法
    // 鼠标左键套索
    // 鼠标右键拖动
    // 鼠标滚轮缩放
  }
  /**
   * 删除右下角权限声明  (建议不要这么做...，特别是大项目)
   *
   * 参考许可证: https://bpmn.io/license/（里面明确说了不允许删除水印）
   */
  removePowerBy() {
    this.container.querySelector('a.bjs-powered-by')?.remove()
  }
  // #region  创建图形相关
  /**
   * 解析并呈现BPMN 2.0图。
   *
   * 完成后，查看器将结果报告给提供的回调函数(err, warnings).
   *
   * ## 生命周期事件
   *
   * 在导入过程中，将触发以下生命周期事件(按顺序):
   *
   *   * import.parse.start (即将从xml读取模型)
   *   * import.parse.complete (模型读取完成)
   *   * import.render.start (图形导入开始)
   *   * import.render.complete (图形导入完成)
   *   * import.done (一切都完成)
   *   * import.finshed (二次封装加上的 by LCM)
   *
   * 您可以使用这些事件与生命周期挂钩。
   *
   * @param {string} xml
   * @param {boolean} updateID 是否需要更新元素id
   * @returns {Promise<ImportXMLResult>}
   */
  importXML(xml, updateID = false) {
    this.clear()

    if (updateID) {
      return super.importXML(xml).then((res) => {
        // let prefix
        let bpmnFactory = this.get('bpmnFactory')

        // 这里是为了解决导出之后再导入，元素ID重复的问题
        this.elementRegistry.forEach((element) => {
          element.businessObject.set('id', '')
          bpmnFactory._ensureId(element.businessObject)
          element.id = element.businessObject.id

          // 请勿修改以下代码
          // 见bpmn-js源码：lib/features/modeling/BpmnFactory.js _ensureId方法（44行）
          // if (is(element, 'bpmn:Activity')) {
          //   prefix = 'Activity'
          // } else if (is(element, 'bpmn:Event')) {
          //   prefix = 'Event'
          // } else if (is(element, 'bpmn:Gateway')) {
          //   prefix = 'Gateway'
          // } else if (isAny(element, ['bpmn:SequenceFlow', 'bpmn:MessageFlow'])) {
          //   prefix = 'Flow'
          // } else {
          //   prefix = (element.$type || '').replace(/^[^:]*:/g, '')
          // }

          // prefix += '_'

          // element.id = this.moddle.ids.nextPrefixed(prefix, element)
        })
        return res
      })
    }

    return super.importXML(xml)
  }
  /**
   * 创建一个新的流程图，只包含一个起始节点
   * @param {String} processId 流程图id
   * @returns {Promise<import('bpmn-js/lib/BaseViewer').ImportXMLResult,import('bpmn-js/lib/BaseViewer').ImportXMLError>}
   */
  async createDiagram(processId = this.moddle.ids.next()) {
    //`Process_${id ? id : Date.now()}`
    const startEventId = this.moddle.ids.next()
    return this.importXML(`<?xml version="1.0" encoding="UTF-8"?>
    <bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                      xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" 
                      xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" 
                      xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" 
                      targetNamespace="http://bpmn.io/schema/bpmn" 
                      id="Definitions_${this.moddle.ids.next()}">
      <bpmn:process id="Process_${processId}" isExecutable="true">
        <bpmn:startEvent id="StartEvent_${startEventId}"/>
      </bpmn:process>
      <bpmndi:BPMNDiagram id="BPMNDiagram_1">
        <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_${processId}">
          <bpmndi:BPMNShape id="StartEvent_${startEventId}_di" bpmnElement="StartEvent_${startEventId}">
            <dc:Bounds height="36.0" width="36.0" x="173.0" y="102.0"/>
          </bpmndi:BPMNShape>
        </bpmndi:BPMNPlane>
      </bpmndi:BPMNDiagram>
    </bpmn:definitions>`)
  }
  // #endregion

  /**
   * 根据名称获取bpmn指定供应商
   * @template {keyof import('../types/provider').BPMNProviderMap} T
   * @param {T} name 名称
   * @param {boolean} [strict] 严格模式，如果为false，则将缺失的服务解析为空
   * @returns {import('../types/provider').BPMNProviderMap[T]}
   */
  get(name, strict) {
    return super.get(name, strict)
  }

  // #region 导出相关
  /**
   * @param {boolean} format 是否格式化
   * @param {Function} callback 回调
   * @returns {Promise<{xml:string}>}
   */
  saveXML(format = false, callback) {
    return super.saveXML({ format }, callback)
  }
  /**
   * @param {Function} callback 回调
   * @returns {Promise<{svg:string}>}
   */
  saveSVG(callback) {
    return super.saveSVG(callback)
  }
  // #endregion

  // #region 操作堆栈相关
  /** 是否可以重做 */
  get canRedo() {
    return this.commandStack?.canRedo()
  }
  /** 是否可以撤销 */
  get canUndo() {
    return this.commandStack?.canUndo()
  }
  /** 撤销 */
  undo() {
    this.commandStack?.undo()
  }
  /** 重做 */
  redo() {
    this.commandStack?.redo()
  }
  // #endregion

  // #region 图形操作相关
  /**
   *
   * @param {'fit-viewport' | 'fit-content' | number} lvl
   * @param {'auto'|import('../types/draw').Point} center
   */
  zoom(lvl, center) {
    this.canvas.zoom(lvl, center)
  }
  /**
   *
   * @param {'left'|'right'|'top'|'bottom'|'middle'|'center'} mode
   */
  align(mode) {
    const align = this.get('alignElements')
    const selection = this.get('selection')
    const elements = selection.get()
    if (!elements || elements.length === 0) {
      return
    }

    align.trigger(elements, mode)
  }
  // #endregion

  // #region 事件相关
  /**
   *
   * @param {import('../types/event').EventName} type
   * @param {any} data
   * @returns
   */
  fire(type, data) {
    return this.eventBus.fire(type, data)
  }
  /**
   * @param {import('../types/event').EventName|Array<import('../types/event').EventName>} events
   * @param {number|import('../types/event').EventCallBack} [priority=1000] 调用该侦听器的优先级越大越高,如果为函数则作为回调
   * @param {import('../types/event').EventCallBack} callback
   * @param {Object} [that] 回调函数的上下文this参数
   */
  on(events, priority, callback, that) {
    return super.on(events, priority, callback, that)
  }
  /**
   * @param {import('../types/event').EventName|Array<import('../types/event').EventName>} events
   * @param {number|import('../types/event').EventCallBack} [priority=1000] 调用该侦听器的优先级越大越高,如果为函数则作为回调
   * @param {import('../types/event').EventCallBack} callback
   * @param {Object} [that] 回调函数的上下文this参数
   */
  once(events, priority, callback, that) {
    return this.eventBus.once(events, priority, callback, that)
  }
  /**
   * 如果没有给出回调，则将删除给定事件名称的所有侦听器。
   * @param {import('../types/event').EventName|Array<import('../types/event').EventName>} events
   * @param {import('../types/event').EventCallBack} callback
   */
  off(events, callback) {
    return super.off(events, callback)
  }
  // #endregion
}
